package com.cipe.cmrs.filter; 

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.zip.GZIPOutputStream;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import com.cipe.cmrs.util.ByteArrayOutputStream2;

/* ------------------------------------------------------------ */
/**
 * GZIP Filter This filter will gzip the content of a response iff:
 * <ul>
 * <li>The filter is mapped to a matching path</li>
 * <li>The response status code is >=200 and <300
 * <li>The content length is unknown or more than the <code>minGzipSize</code>
 * initParameter or the minGzipSize is 0(default)</li>
 * <li>The content-type contain "text", "javascript", "json", "xml", "ecmascript" or the
 * content-type is not "application/gzip"</li>
 * <li>No content-encoding is specified by the resource</li>
 * </ul>
 * 
 * @author Incepio
 * 
 */
public class GzipFilter implements Filter {
	protected int bufferSize = 8192;
	protected int minGzipSize = 0;

	
	public void init(FilterConfig filterConfig) throws ServletException {
		String temp = filterConfig.getInitParameter("bufferSize");
		if (temp != null)
			bufferSize = Integer.parseInt(temp);
		temp = filterConfig.getInitParameter("minGzipSize");
		if (temp != null)
			minGzipSize = Integer.parseInt(temp);
	}// init

	
	public void destroy() {
		// do nothing
	}

	
	public void doFilter(ServletRequest req, ServletResponse res,
			FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		//RequestUtil.setCacheExpireDate(response);
		
		String requestURI = request.getRequestURI();
		
		if (requestURI.endsWith(".png") || requestURI.endsWith(".gif") ||
				requestURI.endsWith(".swf") || requestURI.endsWith(".jpg") ||
				requestURI.endsWith(".jpeg")) {
			chain.doFilter(request, response);
			return;
		}
		
		String ae = request.getHeader("accept-encoding");
		Boolean gzip = (Boolean) request.getAttribute("GzipFilter");
		if (ae != null && ae.indexOf("gzip") >= 0
				&& !response.containsHeader("Content-Encoding")
				&& (gzip == null || gzip.booleanValue())
				&& !"HEAD".equalsIgnoreCase(request.getMethod())) {

			GZIPResponseWrapper wrappedResponse = newGZIPResponseWrapper(request, response);

			boolean exceptional = true;
			try {
				chain.doFilter(request, wrappedResponse);
				exceptional = false;
			} catch (RuntimeException e) {
				request.setAttribute("GzipFilter", Boolean.FALSE);
				if (!response.isCommitted())
					response.reset();
				throw e;
			} finally {
				if (exceptional && !response.isCommitted()) {
					wrappedResponse.resetBuffer();
					wrappedResponse.noGzip();
				} else
					wrappedResponse.finish();
			}
		} else {
			chain.doFilter(request, response);
		}
	}

	protected GZIPResponseWrapper newGZIPResponseWrapper(
			HttpServletRequest request, HttpServletResponse response) {
		return new GZIPResponseWrapper(request, response);
	}

	/*
	 * Allows derived implementations to replace PrintWriter implementation
	 */
	protected PrintWriter newWriter(OutputStream out, String encoding)
			throws UnsupportedEncodingException {
		return encoding == null ? new PrintWriter(out) : new PrintWriter(
				new OutputStreamWriter(out, encoding));
	}

	public class GZIPResponseWrapper extends HttpServletResponseWrapper {
		HttpServletRequest request;
		boolean noGzip;
		PrintWriter writer;
		GzipStream gzStream;
		long contentLength = -1;

		public GZIPResponseWrapper(HttpServletRequest request,
				HttpServletResponse response) {
			super(response);
			this.request = request;
		}

		
		public void setContentType(String ct) {
			super.setContentType(ct);

			if (ct != null) {
				int colon = ct.indexOf(";");
				if (colon > 0)
					ct = ct.substring(0, colon);
			}

			if ((gzStream == null || gzStream.out == null)
					&& ("application/gzip".equalsIgnoreCase(ct) 
							|| (ct == null || !isCompressibleContentType(ct)))) {
				noGzip();
			}
		}//

	    /**
	     * This method return true is the content type contains 
	     * 'text' or 'javascript' or 'ecmascript' or 'xml' or 'json'.
	     * 
	     * @param contentType
	     * @return
	     */
	    private boolean isCompressibleContentType(String contentType) {
	    	if (contentType == null) return false;
	    	final String lowerCaseContentType = contentType.toLowerCase();
	    	
	    	if (lowerCaseContentType.indexOf("image") > -1) {
	    		return false;
	    	}
	    	
	    	if (lowerCaseContentType.indexOf("text") > -1 ||
	    			lowerCaseContentType.indexOf("javascript") > -1 ||
	    			lowerCaseContentType.indexOf("ecmascript") > -1 ||
	    			lowerCaseContentType.indexOf("xml") > -1 ||
	    			lowerCaseContentType.indexOf("json") > -1) {
	    		return true;
	    	}
	    	return false;
	    }//isCompressibleContentType

		
		
		public void setStatus(int sc, String sm) {
			super.setStatus(sc, sm);
			//less than 200 or greater or equal to 300
			if (sc < SC_OK || sc >= SC_MULTIPLE_CHOICES)
				noGzip();
		}

		
		public void setStatus(int sc) {
			super.setStatus(sc);
			if (sc < SC_OK || sc >= SC_MULTIPLE_CHOICES)
				noGzip();
		}

		
		public void setContentLength(int length) {
			contentLength = length;
			if (gzStream != null)
				gzStream.setContentLength(length);
		}

		
		public void addHeader(String name, String value) {
			if ("content-length".equalsIgnoreCase(name)) {
				contentLength = Long.parseLong(value);
				if (gzStream != null)
					gzStream.setContentLength(contentLength);
			} else if ("content-type".equalsIgnoreCase(name)) {
				setContentType(value);
			} else if ("content-encoding".equalsIgnoreCase(name)) {
				super.addHeader(name, value);
				if (!isCommitted()) {
					noGzip();
				}
			} else
				super.addHeader(name, value);
		}

		
		public void setHeader(String name, String value) {
			if ("content-length".equalsIgnoreCase(name)) {
				contentLength = Long.parseLong(value);
				if (gzStream != null)
					gzStream.setContentLength(contentLength);
			} else if ("content-type".equalsIgnoreCase(name)) {
				setContentType(value);
			} else if ("content-encoding".equalsIgnoreCase(name)) {
				super.setHeader(name, value);
				if (!isCommitted()) {
					noGzip();
				}
			} else
				super.setHeader(name, value);
		}

		
		public void setIntHeader(String name, int value) {
			if ("content-length".equalsIgnoreCase(name)) {
				contentLength = value;
				if (gzStream != null)
					gzStream.setContentLength(contentLength);
			} else
				super.setIntHeader(name, value);
		}

		
		public void flushBuffer() throws IOException {
			if (writer != null)
				writer.flush();
			if (gzStream != null)
				gzStream.finish();
			else
				getResponse().flushBuffer();
		}

		
		public void reset() {
			super.reset();
			if (gzStream != null)
				gzStream.resetBuffer();
			writer = null;
			gzStream = null;
			noGzip = false;
			contentLength = -1;
		}

		
		public void resetBuffer() {
			super.resetBuffer();
			if (gzStream != null)
				gzStream.resetBuffer();
			writer = null;
			gzStream = null;
		}

		
		public void sendError(int sc, String msg) throws IOException {
			resetBuffer();
			super.sendError(sc, msg);
		}

		
		public void sendError(int sc) throws IOException {
			resetBuffer();
			super.sendError(sc);
		}

		
		public void sendRedirect(String location) throws IOException {
			resetBuffer();
			super.sendRedirect(location);
		}

		
		public ServletOutputStream getOutputStream() throws IOException {
			if (gzStream == null) {
				if (getResponse().isCommitted() || noGzip)
					return getResponse().getOutputStream();

				gzStream = newGzipStream(request,
						(HttpServletResponse) getResponse(), contentLength,
						bufferSize, minGzipSize);
			} else if (writer != null)
				throw new IllegalStateException("getWriter() called");

			return gzStream;
		}

		
		public PrintWriter getWriter() throws IOException {
			if (writer == null) {
				if (gzStream != null)
					throw new IllegalStateException("getOutputStream() called");

				if (getResponse().isCommitted() || noGzip)
					return getResponse().getWriter();

				gzStream = newGzipStream(request,
						(HttpServletResponse) getResponse(), contentLength,
						bufferSize, minGzipSize);
				writer = newWriter(gzStream, getCharacterEncoding());
			}
			return writer;
		}

		void noGzip() {
			noGzip = true;
			if (gzStream != null) {
				try {
					gzStream.doNotGzip();
				} catch (IOException e) {
					throw new IllegalStateException();
				}
			}
		}

		void finish() throws IOException {
			if (writer != null && !gzStream.closed)
				writer.flush();
			if (gzStream != null)
				gzStream.finish();
		}

		protected GzipStream newGzipStream(HttpServletRequest request,
				HttpServletResponse response, long contentLength,
				int bufferSize, int minGzipSize) throws IOException {
			return new GzipStream(request, response, contentLength, bufferSize,
					minGzipSize);
		}
	}

	public static class GzipStream extends ServletOutputStream {
		protected HttpServletRequest request;
		protected HttpServletResponse response;
		protected OutputStream out;
		protected ByteArrayOutputStream2 bOut;
		protected GZIPOutputStream gzOut;
		protected boolean closed;
		protected int streamBufferSize;
		protected int streamMinGzipSize;
		protected long streamContentLength;

		public GzipStream(HttpServletRequest request,
				HttpServletResponse response, long contentLength,
				int bufferSize, int minGzipSize) throws IOException {
			this.request = request;
			this.response = response;
			this.streamContentLength = contentLength;
			this.streamBufferSize = bufferSize;
			this.streamMinGzipSize = minGzipSize;
			if (minGzipSize == 0)
				doGzip();
		}

		public void resetBuffer() {
			closed = false;
			out = null;
			bOut = null;
			if (gzOut != null && !response.isCommitted())
				response.setHeader("Content-Encoding", null);
			gzOut = null;
		}

		public void setContentLength(long length) {
			streamContentLength = length;
		}

		
		public void flush() throws IOException {
			if (out == null || bOut != null) {
				if (streamContentLength > 0 && streamContentLength < streamMinGzipSize)
					doNotGzip();
				else
					doGzip();
			}

			out.flush();
		}

		
		public void close() throws IOException {
			if (request.getAttribute("javax.servlet.include.request_uri") != null)
				flush();
			else {
				if (bOut != null) {
					if (streamContentLength < 0)
						streamContentLength = bOut.getCount();
					if (streamContentLength < streamMinGzipSize)
						doNotGzip();
					else
						doGzip();
				} else if (out == null) {
					doNotGzip();
				}

				if (gzOut != null)
					gzOut.close();
				else
					out.close();
				closed = true;
			}
		}

		public void finish() throws IOException {
			if (!closed) {
				if (out == null || bOut != null) {
					if (streamContentLength > 0 && streamContentLength < streamMinGzipSize)
						doNotGzip();
					else
						doGzip();
				}

				if (gzOut != null && !closed) {
					closed = true;
					gzOut.close();
				}
			}
		}

		
		public void write(int b) throws IOException {
			checkOut(1);
			out.write(b);
		}

		
		public void write(byte b[]) throws IOException {
			checkOut(b.length);
			out.write(b);
		}

		
		public void write(byte b[], int off, int len) throws IOException {
			checkOut(len);
			out.write(b, off, len);
		}

		protected boolean setContentEncodingGzip() {
			response.setHeader("Content-Encoding", "gzip");
			return response.containsHeader("Content-Encoding");
		}

		public void doGzip() throws IOException {
			if (gzOut == null) {
				if (response.isCommitted())
					throw new IllegalStateException();

				if (setContentEncodingGzip()) {
					out = gzOut = new GZIPOutputStream(response
							.getOutputStream(), streamBufferSize);

					if (bOut != null) {
						out.write(bOut.getBuf(), 0, bOut.getCount());
						bOut = null;
					}
				} else
					doNotGzip();
			}
		}

		public void doNotGzip() throws IOException {
			if (gzOut != null)
				throw new IllegalStateException();
			if (out == null || bOut != null) {
				out = response.getOutputStream();
				if (streamContentLength >= 0) {
					if (streamContentLength < Integer.MAX_VALUE)
						response.setContentLength((int) streamContentLength);
					else
						response.setHeader("Content-Length", Long
								.toString(streamContentLength));
				}

				if (bOut != null)
					out.write(bOut.getBuf(), 0, bOut.getCount());
				bOut = null;
			}
		}

		private void checkOut(int length) throws IOException {
			if (closed)
				throw new IOException("CLOSED");

			if (out == null) {
				if (response.isCommitted()
						|| (streamContentLength >= 0 && streamContentLength < streamMinGzipSize))
					doNotGzip();
				else if (length > streamMinGzipSize)
					doGzip();
				else
					out = bOut = new ByteArrayOutputStream2(streamBufferSize);
			} else if (bOut != null) {
				if (response.isCommitted()
						|| (streamContentLength >= 0 && streamContentLength < streamMinGzipSize))
					doNotGzip();
				else if (length >= (bOut.getBuf().length - bOut.getCount()))
					doGzip();
			}
		}
	}
}
